﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Xml;
using Co9320.NamaAlert.Model;
using Co9320.NamaAlert.Engine.DebugHelper;

namespace Co9320.NamaAlert.Engine
{
	public class LiveCommentServer : IDisposable
	{
		private class ServDatas
		{
			public ServDatas()
			{
				this.Address = null;
				this.Port = 0;
				this.Thread = null;

				this.Communities = new List<string>();
			}

			public void AddCommunities(string communityID)
			{
				this.Communities.Add(communityID);
			}

			public void SetPort(string port)
			{
				this.Port = int.Parse(port);
			}

			public string Address
			{
				get;
				set;
			}

			public int Port
			{
				get;
				private set;
			}

			public string Thread
			{
				get;
				set;
			}

			public List<string> Communities
			{
				get;
				private set;
			}
		}

		#region StaticCreateMethods
		public static LiveCommentServer CreateAuthenticSession(NetworkCredential credential)
		{
			return CreateAuthenticSession(credential, "");
		}

		public static LiveCommentServer CreateAuthenticSession(NetworkCredential credential, string userAgent)
		{
			return CreateAuthenticSession(credential, userAgent, "NicoLiveAlert 1.0.0");
		}

		public static LiveCommentServer CreateAuthenticSession(NetworkCredential credential, string userAgent, string userAgentForGettingAlertStatus)
		{
			string mailAddress = "";
			string password = "";
			if (credential != null)
			{
				mailAddress = credential.UserName;
				password = credential.Password;
			}
			return new LiveCommentServer(mailAddress, password, userAgent, userAgentForGettingAlertStatus);
		}

		public static LiveCommentServer CreateAnonymousSession()
		{
			return CreateAnonymousSession("");
		}

		public static LiveCommentServer CreateAnonymousSession(string userAgent)
		{
			return CreateAnonymousSession(userAgent, "NicoLiveAlert 1.0.0");
		}

		public static LiveCommentServer CreateAnonymousSession(string userAgent, string userAgentForGettingAlertStatus)
		{
			return new LiveCommentServer(userAgent, userAgentForGettingAlertStatus);
		}


		#endregion

		#region SessionControl
		public static string GetTicket(string mailAddress, string password, string userAgent)
		{
			if (mailAddress == null || password == null)
			{
				return null;
			}


			HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create("https://secure.nicovideo.jp/secure/login?site=nicolive_antenna");
			req.Method = "POST";
			req.UserAgent = userAgent;
			req.ContentType = "application/x-www-form-urlencoded";

			string Param = string.Format("mail={0}&password={1}&", mailAddress, password);
			byte[] Buff = UTF8Encoding.UTF8.GetBytes(Param);

			using (var str = req.GetRequestStream())
			{
				str.Write(Buff, 0, Buff.Length);
			}

			XmlDocument doc = new XmlDocument();
			try
			{

				using (WebResponse res = req.GetResponse())
				{
					using (var str = res.GetResponseStream())
					{
						doc.Load(str);
					}
				}
			}
			catch (Exception)
			{
				return null;
			}

			var nav = doc.CreateNavigator();
			var node = nav.SelectSingleNode("/nicovideo_user_response");
			var status = node.GetAttribute("status", string.Empty);

			if (status.ToLower() == "ok")
			{
				node = nav.SelectSingleNode("/nicovideo_user_response/ticket");
				return node.InnerXml;
			}
			else
			{
				return null;
			}
		}

		private static ServDatas GetServerData(string Ticket, string UserAgent)
		{
			if (Ticket == null)
			{
				return null;
			}

			HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create("http://live.nicovideo.jp/api/getalertstatus");
			req.UserAgent = UserAgent;
			req.Method = "POST";
			req.ContentType = "application/x-www-form-urlencoded";

			byte[] Buff = UTF8Encoding.UTF8.GetBytes(string.Format("ticket={0}&", Ticket));

			using (Stream str = req.GetRequestStream())
			{
				str.Write(Buff, 0, Buff.Length);
			}

			XmlDocument doc = new XmlDocument();

			using (WebResponse res = req.GetResponse())
			{
				using (Stream str = res.GetResponseStream())
				{
					doc.Load(str);
				}
			}

			var nav = doc.CreateNavigator();
			var node = nav.SelectSingleNode("/getalertstatus");
			var status = node.GetAttribute("status", string.Empty);

			if (status.ToLower() == "ok")
			{
				ServDatas ret = new ServDatas();

				node = nav.SelectSingleNode("/getalertstatus/ms/addr");
				ret.Address = node.InnerXml;

				node = nav.SelectSingleNode("/getalertstatus/ms/port");
				ret.SetPort(node.InnerXml);

				node = nav.SelectSingleNode("/getalertstatus/ms/thread");
				ret.Thread = node.InnerXml;

				var nodes = nav.Select("/getalertstatus/communities/community_id");

				while (nodes.MoveNext())
				{
					ret.AddCommunities(nodes.Current.InnerXml);
				}


				return ret;
			}
			else
			{
				return null;
			}
		}

		private static ServDatas GetServerData(string userAgent)
		{
			HttpWebRequest req = (HttpWebRequest)WebRequest.Create("http://live.nicovideo.jp/api/getalertinfo");
			req.UserAgent = userAgent;

			XmlDocument doc = new XmlDocument();

			using (var response = req.GetResponse())
			{
				using (var str = response.GetResponseStream())
				{
					doc.Load(str);
				}
			}

			var nav = doc.CreateNavigator();
			var node = nav.SelectSingleNode("/getalertstatus");
			string Status = node.GetAttribute("status", string.Empty);

			if (Status.ToLower() == "ok")
			{
				ServDatas ret = new ServDatas();

				node = nav.SelectSingleNode("/getalertstatus/ms/addr");
				ret.Address = node.InnerXml;

				node = nav.SelectSingleNode("/getalertstatus/ms/port");
				ret.SetPort(node.InnerXml);

				node = nav.SelectSingleNode("/getalertstatus/ms/thread");
				ret.Thread = node.InnerXml;

				return ret;

			}
			else
			{
				return null;
			}

		}

		private static List<string> RefreshCommunities(string mailAddress, string password, string UserAgent1, string UserAgent2)
		{
			List<string> ret = new List<string>();

			string tkt = GetTicket(mailAddress, password, UserAgent1);

			if (tkt == null)
			{
				return ret;
			}

			try
			{
				ServDatas dta = GetServerData(tkt, UserAgent2);
				return dta.Communities;
			}
			catch
			{
				return new List<string>();
			}
		}
		#endregion

		private readonly TimeSpan DefaultDelay_ = new TimeSpan(0, 1, 30);
		private readonly TimeSpan DefaultTimeOut_ = new TimeSpan(0, 1, 0);

		public event EventHandler<LiveProgramEventArgs> LiveProgramStarted;
		public event EventHandler AuthenticationFailed;

		private Thread WorkingThread_;
		private MemoryStream Buffer_;
		private TcpClient client_;
		private Semaphore QueryGate_;
		private Semaphore ThreadPoolSemaphore;
		private HashSet<string> Inspector_;
		private ReadOnlyCollection<string> RegisteredStationIds_;


		private WaitCallback CallBack_;

		private LiveCommentServer(string UserAgent1, string UserAgent2)
		{
			QueryGate_ = new Semaphore(2, 2); // 同時コネクション数は2まで
			ThreadPoolSemaphore = new Semaphore(20, 20); // 同時にThreadPoolへ追加するアイテムは20個まで(Mono ThreadPool deadlock 対策)
			this.WorkingThread_ = new Thread(new ThreadStart(this.ThreadMain));
			this.WorkingThread_.Name = "ApiRecieverWorker";
			this.WorkingThread_.IsBackground = true;

			this.Buffer_ = new MemoryStream();
			CallBack_ = new WaitCallback(EventSend);

			this.MailAdddress = null;
			this.Password = null;
			this.IsStarted = false;
			client_ = null;
			Inspector_ = new HashSet<string>();
			RegisteredStationIds_ = new ReadOnlyCollection<string>(new List<string>());

			this.UserAgent = UserAgent1;
			this.UserAgentForGettingAlertStatus = UserAgent2;

			EnableFiltering = true;
		}


		private LiveCommentServer(string mailAddress, string password, string UserAgent1, string UserAgent2) :
			this(UserAgent1, UserAgent2)
		{
			this.MailAdddress = mailAddress;
			this.Password = password;
			this.Inspector_ = new HashSet<string>();

			this.RegisteredStationIds_ = new ReadOnlyCollection<string>(RefreshCommunities(mailAddress, password, UserAgent1, UserAgent2));

			this.UserAgent = UserAgent1;
			this.UserAgentForGettingAlertStatus = UserAgent2;
			EnableFiltering = true;
		}


		public bool EnableFiltering
		{
			get;
			set;
		}

		public string UserAgent
		{
			get;
			private set;
		}

		public string UserAgentForGettingAlertStatus
		{
			get;
			private set;
		}

		public bool IsAnonymousSession
		{
			get
			{
				if (MailAdddress == null || Password == null)
				{
					return true;
				}
				else
				{
					return false;
				}
			}
		}

		public bool IsStarted
		{
			get;
			private set;
		}

		public string MailAdddress
		{
			get;
			private set;
		}

		public string Password
		{
			get;
			private set;
		}

		public System.Collections.ObjectModel.ReadOnlyCollection<string> RegisteredStationIds
		{
			get
			{
				return this.RegisteredStationIds_;
			}
		}

		public void RefreshCommunities()
		{
			if (!IsAnonymousSession)
			{
				this.RegisteredStationIds_ = new ReadOnlyCollection<string>(RefreshCommunities(this.MailAdddress, this.Password,
					this.UserAgent, this.UserAgentForGettingAlertStatus));
			}
		}


		public void RegisterNotifiedStation(string id)
		{
			if (id != null)
			{
				lock (Inspector_)
				{
					if (!Inspector_.Contains(id))
					{
						Inspector_.Add(id);
					}
				}
			}
		}



		public void Start()
		{
			if (IsStarted)
			{
				return;
			}

			this.IsStarted = true;

			WorkingThread_.Start();

		}

		private void ThreadMain()
		{
			try
			{
				LogOutputter.WriteElement(LogMode.Notify, "API Thread start");

				if (this.RegisteredStationIds.Count == 0)
				{
					this.RefreshCommunities();
				}

				for (; ; )
				{
					ServDatas ServDta = null;

					try
					{
						if (IsAnonymousSession)
						{
							LogOutputter.WriteElement(LogMode.Notify, "Anonymouse session");
							ServDta = GetServerData(this.UserAgent);
						}
						else
						{
							LogOutputter.WriteElement(LogMode.Notify, "Authentic session");
							// TODO 既に Create時に1度呼んだものを利用
							string tkt = GetTicket(MailAdddress, Password, UserAgent);
							if (tkt != null)
							{
								ServDta = GetServerData(tkt);
							}
						}

						if (ServDta == null)
						{
							OnAuthenticationFailed(this, EventArgs.Empty);
							goto ROOP_BREAK;
						}

						LogOutputter.WriteElement(LogMode.Notify, "create session completed");
					}
					catch (Exception ex)
					{
						LogOutputter.WriteElement(LogMode.Warning, "Create session failed");
						LogOutputter.WriteElement(LogMode.Warning, ex);
						Thread.Sleep(DefaultDelay_);
						RefreshCommunities();
						continue;
					}




					client_ = new TcpClient(ServDta.Address, ServDta.Port);
					client_.ReceiveTimeout = (int)DefaultTimeOut_.TotalMilliseconds;

					string reqstr = string.Format("<thread thread=\"{0}\" version=\"20061206\" res_from=\"-200\"/>\0", ServDta.Thread);

					Stream str = client_.GetStream();

					byte[] buff = UTF8Encoding.UTF8.GetBytes(reqstr);
					str.Write(buff, 0, buff.Length);
					str.Flush();

					buff = null;

					MemoryStream memstr = new MemoryStream();
					StreamReader rdr = new StreamReader(memstr);

					for (; ; )
					{
						if (!IsStarted)
						{
							goto ROOP_BREAK;
						}

						int cache = -1;

						try
						{
							cache = str.ReadByte();
						}
						catch (Exception ex)
						{
							LogOutputter.WriteElement(LogMode.Warning, ex);
							client_.Close();
							break;
						}

						if (cache == 0)
						{
							memstr.Position = 0;
							XmlDocument doc = new XmlDocument();
							doc.Load(memstr);
							ThreadPoolSemaphore.WaitOne();
							ThreadPool.QueueUserWorkItem(CallBack_, doc);
							memstr.SetLength(0);
						}
						else if (cache == -1)
						{
							break;
						}
						else
						{
							memstr.WriteByte((byte)cache);
						}
					}
				}

			ROOP_BREAK:
				return;
			}
			catch (ThreadAbortException)
			{
				try
				{
					client_.Close();
				}
				catch
				{
				}

				return;
			}
		}


		private void EventSend(object param)
		{
			XmlDocument doc = param as XmlDocument;
			try
			{
				if (doc == null)
				{
					return;
				}

				string[] Values = doc.InnerText.Split(',');

				if (Values.Length != 3)
				{
					return;
				}

				if (!Inspector_.Contains(Values[1]) && EnableFiltering)
				{
					return;
				}

				LiveProgram program = Query(Values[0], Values[2], UserAgent);

				if (program != null)
				{
					OnLiveProgramStarted(this, new LiveProgramEventArgs(program));
				}
			}
			catch(Exception ex)
			{
				LogOutputter.WriteElement(LogMode.Warning, ex);
				//No need to do anything.
			}
			finally
			{
				ThreadPoolSemaphore.Release();
			}

		}


		private LiveProgram Query(string LiveID, string CasterID, string UserAgent)
		{
			XmlDocument doc = new XmlDocument();

			QueryGate_.WaitOne();

			WebClient client = new WebClient();

			HttpWebRequest req = (HttpWebRequest)WebRequest.Create(string.Format("http://live.nicovideo.jp/api/getstreaminfo/lv{0}", LiveID));
			req.UserAgent = UserAgent;


			using (WebResponse res = req.GetResponse())
			{
				using (Stream str = res.GetResponseStream())
				{
					doc.Load(str);
				}
			}

			QueryGate_.Release();

			var nav = doc.SelectSingleNode("/getstreaminfo").CreateNavigator();

			string Content = nav.GetAttribute("status", string.Empty);

			if (Content.ToLower() != "ok")
			{
				return null;
			}

			LiveProgram ret = new LiveProgram();

			//SetID
			Content = nav.SelectSingleNode("request_id").InnerXml;

			if (Content == null || Content.Length == 0)
			{
				return null;
			}
			ret.Id = int.Parse(LiveID);
			ret.CasterId = int.Parse(CasterID);


			nav = nav.SelectSingleNode("streaminfo");


			//Title
			Content = nav.SelectSingleNode("title").InnerXml;
			ret.Title = Content;

			//Description
			Content = nav.SelectSingleNode("description").InnerXml;
			ret.ShortContent = Content;

			//CommunityID
			Content = nav.SelectSingleNode("default_community").InnerXml;
			ret.Station.Id = Content;

			//Set community or channel name.
			nav = nav.SelectSingleNode("/getstreaminfo/communityinfo");
			Content = nav.SelectSingleNode("name").InnerXml;
			ret.Station.Name = Content;

			Content = nav.SelectSingleNode("thumbnail").InnerXml;
			ret.Station.IconUri = new Uri(Content);

			//Set channel or community thumbnail graphic url.
			

			//Set misc...
			ret.IsOnAir = true;
			ret.StartDateTime = DateTime.Now;
			ret.CrawlingDateTime = DateTime.Now;
			ret.CasterName = null;

			return ret;
		}

		private void OnLiveProgramStarted(object Sender, LiveProgramEventArgs e)
		{
			if (LiveProgramStarted != null)
			{
				LiveProgramStarted(Sender, e);
			}
		}

		private void OnAuthenticationFailed(object sender, EventArgs e)
		{
			if (AuthenticationFailed != null)
			{
				AuthenticationFailed(sender, e);
			}
		}


		#region IDisposable メンバ

		public void Dispose()
		{
			IsStarted = false;
			WorkingThread_.Abort();
		}

		#endregion
	}
}
